iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Modern Web

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站系列 第 4

Day 04|四行 Playwright,藏著哪些畫面依賴?

  • 分享至 

  • xImage
  •  

Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?

安安~我是ChiYu~

昨天我把活動網站從搜尋一路走到取消報名,最後挑了風險最低的搜尋流程,準備交給
Playwright 重播。今天它正式接班:開啟頁面、填入 WebMCP、按下搜尋,再確認畫面出現
一場活動。

第一輪很順,四行程式碼就完成了。接著我只在搜尋按鈕外面多包一層元素,畫面看起來一樣,
人類也照樣能按。這時兩種 Locator 卻走向不同結果:依 role 與 accessible name 找按鈕的
測試繼續通過,寫死父子結構的 CSS selector 則直接找不到目標。

這個改動小到很容易被 code review 略過,卻剛好適合回答今天的問題:Playwright 到底是
怎麼「看見」頁面上的按鈕?

本系列使用的 Locator Lab 保留在:

/labs/day-04-playwright-locator/index.html

用四行 Playwright 重播昨天的活動搜尋

我先把昨天的手動操作改寫成下面這支測試:

test("the original accessible-name locator performs the search", async ({ page }) => {
  await page.goto("/labs/day-04-playwright-locator/index.html");
  await page.getByLabel("關鍵字").fill("WebMCP");
  await page.getByRole("button", { name: "搜尋活動" }).click();
  await expect(page.getByRole("status"))
    .toContainText("人類操作已完成:1 場");
});

它的操作順序很單純:

  1. 開啟固定的 Lab。
  2. <label> 找到關鍵字欄位,填入 WebMCP
  3. button role 與 accessible name 找到「搜尋活動」。
  4. 等待 status 顯示一場搜尋結果。

這四行也把腳本依賴的條件寫了出來。網址要正確、欄位要有可辨認的 label、按鈕要有符合的
role 與名稱,最後還要有一個可驗收的畫面結果。少了任何一項,Playwright 都不會像人類一樣
「看起來差不多就按下去」。

Playwright 官方 Locator 文件建議優先使用面向
使用者的屬性與明確契約。getByLabel() 適合表單欄位;getByRole() 則會依 ARIA role、
相關屬性與 accessible name 尋找元素。這比記住畫面座標,更接近使用者與輔助技術理解
介面的方式。

accessible name 不一定只來自肉眼看到的文字。瀏覽器可能依元素內容、aria-label
aria-labelledby 或關聯的 <label> 計算名稱。所以 DOM 裡有一顆 button 還不夠,
它的 role 和計算後名稱都要符合 Locator。

第一次執行後,測試找到「搜尋活動」按鈕,也等到一場結果出現在 status:

Playwright 依 accessible name 成功完成搜尋

圖 1:Locator、目前 DOM 與可見結果放在同一張實測畫面中。這是 E2 browser test,不是 Agent trace。

按鈕多包一層後,role Locator 通過、CSS selector 失敗

第一輪綠燈後,我開啟另一個變體:

/labs/day-04-playwright-locator/index.html?variant=wrapped

這個版本只在按鈕外新增 .action-shell。按鈕還是按鈕,accessible name 也仍然是「搜尋活動」,
所以這行照常找得到:

page.getByRole("button", { name: "搜尋活動" })

但直接指定父子關係的 #search-form > button 會失敗。原本按鈕是 form 的直接子元素,多包
一層後,這個 DOM 關係已經不存在了。

人類從頭到尾沒感覺到差異,測試卻因為「怎麼找按鈕」而得到不同答案。這不是哪種寫法永遠
比較高級,而是兩支 Locator 在保護不同契約:role Locator 保護使用者能感知的語意,CSS
selector 保護指定的 DOM 結構。

Locator 它依賴的線索 介面改動後可能發生什麼
getByRole('button', { name }) role 與 accessible name 改文案會失敗,外層包裝通常不影響
getByLabel('關鍵字') label 與表單控制項的關聯 移除 label 或改名稱會失敗
#search-form > button 固定的父子 DOM 結構 多包一層元素就失敗
[data-action='search-events'] 團隊定義的穩定屬性 UI 可以改,但團隊要維護這份契約

這次我保留 getByRole(),因為讀者版測試真正要守住的是「使用者仍能找到並操作搜尋按鈕」。
如果某支測試的目的就是限制 DOM 階層,那麼 CSS selector 才是合理選擇。Locator 不能只挑
最短的,要先問這支測試究竟想保護哪件事。

Playwright 的 Locator 也不是在測試開始時抓住同一個節點不放。每次操作時,它都會重新查詢
當下 DOM,並搭配 auto-wait 與 retry。這能處理正常的非同步 render,卻不會替工程師猜測
「探索場次」是不是等同原本的「搜尋活動」。那是明天要故意留下的另一個坑。

重跑 Locator Lab,同時保存成功與預期失敗

要重播這個實驗,先啟動專案:

npm run dev

接著開啟原始 Lab,手動搜尋一次:

http://127.0.0.1:5173/labs/day-04-playwright-locator/index.html

最後執行 focused spec:

npx playwright test tests/browser/reader-facing-labs.spec.ts --grep "Day 4" --reporter=line

這一輪得到 4 passed。前兩題驗證原始 accessible-name Locator 與新增 DOM wrapper 後的結果;
後兩題會刻意使用錯誤 Locator,捕捉 timeout,再換回正確 Locator 完成復原。換句話說,整份
spec 是綠燈,不代表過程中從未失敗,而是預期中的失敗也有被測試接住。

我也沒有把驗收停在「click() 沒丟例外」。最後一行會等待 role="status" 顯示
「人類操作已完成:1 場」。如果按鈕存在,submit handler 卻沒有接上,點擊動作可能照樣完成,
結果斷言仍會把問題攔下來。

想逐步看瀏覽器怎麼操作,可以加上 debug 模式:

npx playwright test tests/browser/reader-facing-labs.spec.ts --grep "Day 4" --debug --workers=1

Playwright 也能保存 trace,讓我們在失敗後回頭看 action、DOM snapshot 與錯誤位置。
Trace Viewer 官方文件提供以下開啟方式:

npx playwright show-trace <trace.zip>

一張畫面只能告訴我「當時長這樣」,trace 則能繼續回答:用了哪個 Locator、頁面上有哪些
節點、等待花了多久,以及錯誤發生在操作前還是操作後。明天按鈕改名後,這些資訊會用來判斷
搜尋功能真的壞了,還是舊名稱讓腳本找不到入口。

Playwright 能重播 UI 劇本,還不會自己理解任務

回頭看今天的四行測試,Playwright 很忠實地完成了工程師指定的步驟。DOM 多包一層時,
getByRole() 仍能依使用者可感知的語意找到按鈕,這也確認了我選擇這支 Locator 的理由。

但它沒有自己理解「幫我找活動」。網址、欄位、按鈕名稱與成功條件,全部是我事先寫進劇本的。
今天證明的是 UI 流程可以在瀏覽器裡重播,也就是 E2 browser test;它還不是 Agent 看見任務後
自行選擇 Tool 的證據。

明天我會把按鈕的「搜尋活動」改成「探索場次」,資料與 submit handler 都不碰。人類仍然
知道該按哪裡,今天這支依舊名稱找按鈕的腳本卻會停在 click 以前。到時候我們就能把「網站
壞掉」和「Locator 過期」拆成兩件事來看。


上一篇
Day 03|我先把 Inspector 關掉,自己走一次活動網站
下一篇
Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
系列文
別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言